{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "让我们首先直接使用模型。ChatModel 是 LangChain “运行接口” 的实例，这意味着它们提供了一个标准接口供我们与之交互。要简单地调用模型，我们可以将消息列表传递给.`invoke` 方法。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Hi Bob! ٩(◕‿◕｡)۶ How can I assist you today?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 17, 'total_tokens': 38, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_name': 'qwen-plus', 'system_fingerprint': None, 'id': 'chatcmpl-8ba32a1f-771c-9726-a176-c6bdf9d9752d', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--ad9e7ee4-f83d-4a7f-ac20-e279b6f62810-0', usage_metadata={'input_tokens': 17, 'output_tokens': 21, 'total_tokens': 38, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "\n",
    "OPENAI_API_KEY = \"sk-78f80b100afb41c9abed4875cf4446e9\"\n",
    "\n",
    "model = ChatOpenAI(\n",
    "    model=\"qwen-plus\",\n",
    "    openai_api_key=OPENAI_API_KEY,\n",
    "    openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n",
    ")\n",
    "\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "model.invoke([HumanMessage(content=\"Hi! I'm Bob\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "模型本身没有任何状态概念。例如，如果问一个后续问题，它将没有将之前的对话轮次作为上下文，因此无法回答问题。 这会导致糟糕的聊天机器人体验！"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content=\"As an AI, I don't have access to personal information, so I can't possibly know your name. If you're looking to generate a random name or need suggestions for a username or something similar, feel free to let me know, and I can help with that!\", additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 57, 'prompt_tokens': 10, 'total_tokens': 67, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'glm-4', 'system_fingerprint': None, 'id': '202506261429087bb048274bb7461b', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--ee4da567-3fed-4003-b3ee-9bae535ac687-0', usage_metadata={'input_tokens': 10, 'output_tokens': 57, 'total_tokens': 67, 'input_token_details': {}, 'output_token_details': {}})"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "model.invoke([HumanMessage(content=\"What's my name?\")])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "为了绕过这个问题，我们需要将整个对话历史传递给模型。让我们看看这样做会发生什么："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "AIMessage(content='Your name is Bob, as you mentioned earlier. How can I help you with anything else?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 21, 'prompt_tokens': 28, 'total_tokens': 49, 'completion_tokens_details': None, 'prompt_tokens_details': None}, 'model_name': 'glm-4', 'system_fingerprint': None, 'id': '202506261445552a5c68867a8d4e05', 'service_tier': None, 'finish_reason': 'stop', 'logprobs': None}, id='run--09c4d314-d13e-45ac-8172-ff933da4f8c8-0', usage_metadata={'input_tokens': 28, 'output_tokens': 21, 'total_tokens': 49, 'input_token_details': {}, 'output_token_details': {}})"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.messages import AIMessage\n",
    "\n",
    "model.invoke(\n",
    "    [\n",
    "        HumanMessage(content=\"Hi! I'm Bob\"),\n",
    "        AIMessage(content=\"Hello Bob! How can I assist you today?\"),\n",
    "        HumanMessage(content=\"What's my name?\"),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 消息历史"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们可以使用消息历史类来包装我们的模型，使其具有状态。 这将跟踪模型的输入和输出，并将其存储在某个数据存储中。 未来的交互将加载这些消息，并将其作为输入的一部分传递给链。 让我们看看如何使用这个！\n",
    "\n",
    "首先，让我们确保安装 `langchain-community` ，因为我们将使用其中的集成来存储消息历史。\n",
    "\n",
    "之后，我们可以导入相关类并设置我们的链，该链包装模型并添加此消息历史。这里的一个关键部分是我们作为 `get_session_history` 传入的函数。这个函数预计接受一个 `session_id` 并返回一个消息历史对象。这个 `session_id` 用于区分不同的对话，并应作为配置的一部分在调用新链时传入（我们将展示如何做到这一点）。\n",
    "\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.chat_history import (\n",
    "    BaseChatMessageHistory,\n",
    "    InMemoryChatMessageHistory,\n",
    ")\n",
    "from langchain_core.runnables.history import RunnableWithMessageHistory\n",
    "\n",
    "store = {}\n",
    "\n",
    "\n",
    "def get_session_history(session_id: str) -> BaseChatMessageHistory:\n",
    "    if session_id not in store:\n",
    "        store[session_id] = InMemoryChatMessageHistory()\n",
    "    return store[session_id]\n",
    "\n",
    "\n",
    "with_message_history = RunnableWithMessageHistory(model, get_session_history)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "我们现在需要创建一个 `config` ，每次都传递给可运行的部分。这个配置包含的信息并不是直接作为输入的一部分，但仍然是有用的。在这种情况下，我们想要包含一个 `session_id` 。这应该看起来像："
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"session_id\": \"abc2\"}}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Hello Bob! How can I assist you today? If you have any questions or need information on a topic, feel free to ask.'"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = with_message_history.invoke(\n",
    "    [HumanMessage(content=\"Hi! I'm Bob\")],\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Your name is Bob, as you mentioned earlier. If you have any other questions or need assistance with something else, feel free to let me know!'"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = with_message_history.invoke(\n",
    "    [HumanMessage(content=\"What's my name?\")],\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "太好了！我们的聊天机器人现在记住了关于我们的事情。如果我们更改配置以引用不同的 `session_id`，我们可以看到它开始新的对话。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"As an AI, I don't have access to personal data or the ability to know your name unless you've provided it to me in our conversation. If you're looking for a random or placeholder name, I can generate one for you, but if you're asking for your real name, please provide it so I can address you accordingly.\""
      ]
     },
     "execution_count": 16,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "config = {\"configurable\": {\"session_id\": \"abc3\"}}\n",
    "\n",
    "response = with_message_history.invoke(\n",
    "    [HumanMessage(content=\"What's my name?\")],\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "然而，我们始终可以回到原始对话（因为我们将其保存在数据库中）"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "\"You said your name is Bob. If you're asking for confirmation or if there's something else you'd like to know, please let me know!\""
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "config = {\"configurable\": {\"session_id\": \"abc2\"}}\n",
    "\n",
    "response = with_message_history.invoke(\n",
    "    [HumanMessage(content=\"What's my name?\")],\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "这就是我们如何支持聊天机器人与多个用户进行对话的方式！\n",
    "\n",
    "现在，我们所做的只是为模型添加了一个简单的持久化层。我们可以通过添加提示词模板来使其变得更加复杂和个性化。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 提示词模板"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "提示词模板帮助将原始用户信息转换为大型语言模型可以处理的格式。在这种情况下，原始用户输入只是一个消息，我们将其传递给大型语言模型。现在让我们使其变得更复杂一些。首先，让我们添加一个带有一些自定义指令的系统消息（但仍然将消息作为输入）。接下来，我们将添加除了消息之外的更多输入。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "首先，让我们添加一个系统消息。为此，我们将创建一个 `ChatPromptTemplate`。我们将利用 `MessagesPlaceholder` 来传递所有消息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder\n",
    "\n",
    "prompt = ChatPromptTemplate.from_messages(\n",
    "    [\n",
    "        (\n",
    "            \"system\",\n",
    "            \"You are a helpful assistant. Answer all questions to the best of your ability in {language}.\",\n",
    "        ),\n",
    "        MessagesPlaceholder(variable_name=\"messages\"),\n",
    "    ]\n",
    ")\n",
    "\n",
    "chain = prompt | model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "请注意，我们在提示中添加了一个新的 `language` 输入。我们现在可以调用链并传入我们选择的语言。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'你好，Bob！有什么我可以帮助你的吗？'"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = chain.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"hi! I'm bob\")], \"language\": \"Chinese\"}\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在让我们将这个更复杂的链封装在一个消息历史类中。这次，由于输入中有多个键，我们需要指定正确的键来保存聊天历史。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "with_message_history = RunnableWithMessageHistory(\n",
    "    chain,\n",
    "    get_session_history,\n",
    "    input_messages_key=\"messages\",\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [],
   "source": [
    "config = {\"configurable\": {\"session_id\": \"abc11\"}}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'你好，托德！有什么可以帮助你的吗？如果你有任何问题或需要协助，请随时告诉我。'"
      ]
     },
     "execution_count": 32,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = with_message_history.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"hi! I'm todd\")], \"language\": \"Chinese\"},\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'你之前告诉我你的名字是Todd。所以，你的名字是Todd。如果你有其他问题或需要帮助，请继续提问。'"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "response = with_message_history.invoke(\n",
    "    {\"messages\": [HumanMessage(content=\"whats my name?\")], \"language\": \"Chinese\"},\n",
    "    config=config,\n",
    ")\n",
    "\n",
    "response.content"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 流式处理"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "现在我们有了一个功能齐全的聊天机器人。然而，对于聊天机器人应用程序来说，一个非常重要的用户体验考虑是流式处理。大型语言模型有时可能需要一段时间才能响应，因此为了改善用户体验，大多数应用程序所做的一件事是随着每个令牌的生成流回。这样用户就可以看到进度。\n",
    "\n",
    "实际上，这非常简单！\n",
    "\n",
    "所有链都暴露一个 `.stream` 方法，使用消息历史的链也不例外。我们可以简单地使用该方法获取流式响应。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "嗨|，|Todd|！|这里|有个|笑话|给你|：\n",
      "\n",
      "有一天|，|一只|乌龟|走进|了一家|酒吧|，|跟|酒|保|说|：“|请|给我|来|杯|热|腾|腾|的|牛肉|汤|。”|\n",
      "酒|保|有些|惊讶|地看着|乌龟|说|：“|你|确定|你要|牛肉|汤|？|但是我们|这里是|酒吧|啊|。”|\n",
      "乌龟|回答|：“|我知道|，|但是|这么|冷的|天气|，|再|喝酒|我怕|冻|成|龟|汤|了|。”||"
     ]
    }
   ],
   "source": [
    "config = {\"configurable\": {\"session_id\": \"abc15\"}}\n",
    "for r in with_message_history.stream(\n",
    "    {\n",
    "        \"messages\": [HumanMessage(content=\"hi! I'm todd. tell me a joke\")],\n",
    "        \"language\": \"Chinese\",\n",
    "    },\n",
    "    config=config,\n",
    "):\n",
    "    print(r.content, end=\"|\")"
   ]
  }
 ],
 "metadata": {
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
